--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 39b8d5fef81d42e77ebf39f5f8292d2afc643d04
Parents : df0a7e1
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-02-17T17:59:46-06:00
Cleanup
Changes
14 files changed, 159 insertions(+), 140 deletions(-)
Diff
diff --git a/README.md b/README.md
index e2a48fda..0c3ab8b7 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ docker compose up -d
```
### Docker Permissions Note
+
If you encounter a `PermissionError` when running the Docker container, it's likely because the container's user (UID 1000) doesn't have permission to write to your host's `./meshchat-config` folder. You can fix this by running:
```bash
@@ -246,36 +247,36 @@ MeshChatX can be configured via command-line arguments or environment variables.
We use [Task](https://taskfile.dev/) for automation.
-| Task | Description |
-| :----------------------------- | :--------------------------------------------- |
-| `task install` | Install all dependencies |
-| `task run` | Run the application |
-| `task dev` | Run the application in development mode |
-| `task lint:all` | Run all linters (Python & Frontend) |
-| `task lint:be` | Lint Python code only |
-| `task lint:fe` | Lint frontend code only |
-| `task fmt:all` | Format all code (Python & Frontend) |
-| `task fmt:be` | Format Python code only |
-| `task fmt:fe` | Format frontend code only |
-| `task test:all` | Run all tests |
-| `task test:cov` | Run tests with coverage reports |
-| `task test:be` | Run Python tests only |
-| `task test:fe` | Run frontend tests only |
-| `task build:all` | Build frontend and backend |
-| `task build:fe` | Build only the frontend |
-| `task build:wheel` | Build Python wheel package |
-| `task compile` | Compile Python code to check for syntax errors |
-| `task docker:build` | Build Docker image using buildx |
-| `task docker:run` | Run Docker container using docker-compose |
-| `task dist:linux:appimage` | Build Linux AppImage |
-| `task dist:win:exe` | Build Windows portable executable |
-| `task dist:win:wine` | Build Windows portable (Wine cross-build) |
-| `task dist:all` | Build all Electron apps |
-| `task dist:all:wine` | Build all Electron apps (Wine cross-build) |
-| `task android:prepare` | Prepare Android build |
-| `task android:build` | Build Android APK |
-| `task dist:fe:flatpak` | Build Flatpak package |
-| `task clean` | Clean build artifacts and dependencies |
+| Task | Description |
+| :------------------------- | :--------------------------------------------- |
+| `task install` | Install all dependencies |
+| `task run` | Run the application |
+| `task dev` | Run the application in development mode |
+| `task lint:all` | Run all linters (Python & Frontend) |
+| `task lint:be` | Lint Python code only |
+| `task lint:fe` | Lint frontend code only |
+| `task fmt:all` | Format all code (Python & Frontend) |
+| `task fmt:be` | Format Python code only |
+| `task fmt:fe` | Format frontend code only |
+| `task test:all` | Run all tests |
+| `task test:cov` | Run tests with coverage reports |
+| `task test:be` | Run Python tests only |
+| `task test:fe` | Run frontend tests only |
+| `task build:all` | Build frontend and backend |
+| `task build:fe` | Build only the frontend |
+| `task build:wheel` | Build Python wheel package |
+| `task compile` | Compile Python code to check for syntax errors |
+| `task docker:build` | Build Docker image using buildx |
+| `task docker:run` | Run Docker container using docker-compose |
+| `task dist:linux:appimage` | Build Linux AppImage |
+| `task dist:win:exe` | Build Windows portable executable |
+| `task dist:win:wine` | Build Windows portable (Wine cross-build) |
+| `task dist:all` | Build all Electron apps |
+| `task dist:all:wine` | Build all Electron apps (Wine cross-build) |
+| `task android:prepare` | Prepare Android build |
+| `task android:build` | Build Android APK |
+| `task dist:fe:flatpak` | Build Flatpak package |
+| `task clean` | Clean build artifacts and dependencies |
## Security
diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index 05ec0c14..9e65820d 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -1095,8 +1095,7 @@ export default {
const status = this.propagationNodeStatus?.state;
const messagesReceived = this.propagationNodeStatus?.messages_received ?? 0;
const messagesStored = this.propagationNodeStatus?.messages_stored ?? 0;
- const deliveryConfirmations =
- this.propagationNodeStatus?.delivery_confirmations ?? 0;
+ const deliveryConfirmations = this.propagationNodeStatus?.delivery_confirmations ?? 0;
const messagesHidden = this.propagationNodeStatus?.messages_hidden ?? 0;
if (status === "complete" || status === "idle") {
const base = this.$t("app.sync_complete", { count: messagesReceived });
diff --git a/meshchatx/src/frontend/components/interfaces/Interface.vue b/meshchatx/src/frontend/components/interfaces/Interface.vue
index 75d26153..1cd6c07e 100644
--- a/meshchatx/src/frontend/components/interfaces/Interface.vue
+++ b/meshchatx/src/frontend/components/interfaces/Interface.vue
@@ -38,7 +38,9 @@
</div>
<div class="flex-1 min-w-0 space-y-2 overflow-hidden">
<div class="flex items-center gap-2 flex-wrap">
- <div class="text-lg font-semibold text-gray-900 dark:text-white truncate min-w-0">{{ iface._name }}</div>
+ <div class="text-lg font-semibold text-gray-900 dark:text-white truncate min-w-0">
+ {{ iface._name }}
+ </div>
<span class="type-chip shrink-0">{{ iface.type }}</span>
<span :class="statusChipClass" class="shrink-0">{{
isInterfaceEnabled(iface) ? $t("app.enabled") : $t("app.disabled")
@@ -48,34 +50,40 @@
<div class="text-sm text-gray-600 dark:text-gray-300 break-words min-w-0">
{{ description }}
</div>
- <div class="flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300">
- <span v-if="iface._stats?.bitrate" class="stat-chip"
- >{{ $t("interface.bitrate") }} {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</span
- >
- <span class="stat-chip">{{ $t("interface.tx") }} {{ formatBytes(iface._stats?.txb ?? 0) }}</span>
- <span class="stat-chip">{{ $t("interface.rx") }} {{ formatBytes(iface._stats?.rxb ?? 0) }}</span>
- <span v-if="iface.type === 'RNodeInterface' && iface._stats?.noise_floor" class="stat-chip"
- >{{ $t("interface.noise") }} {{ iface._stats?.noise_floor }} dBm</span
- >
- <span v-if="iface._stats?.clients != null" class="stat-chip"
- >{{ $t("interface.clients") }} {{ iface._stats?.clients }}</span
- >
- </div>
- <div v-if="iface._stats?.ifac_signature" class="ifac-line">
- <span class="text-emerald-500 font-semibold">{{ iface._stats.ifac_size * 8 }}-bit IFAC</span>
- <span v-if="iface._stats?.ifac_netname">• {{ iface._stats.ifac_netname }}</span>
- <span>•</span>
- <button
- type="button"
- class="text-blue-500 hover:underline"
- @click="onIFACSignatureClick(iface._stats.ifac_signature)"
- >
- {{ iface._stats.ifac_signature.slice(0, 8) }}…{{ iface._stats.ifac_signature.slice(-8) }}
- </button>
- </div>
+ <div class="flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300">
+ <span v-if="iface._stats?.bitrate" class="stat-chip"
+ >{{ $t("interface.bitrate") }} {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</span
+ >
+ <span class="stat-chip"
+ >{{ $t("interface.tx") }} {{ formatBytes(iface._stats?.txb ?? 0) }}</span
+ >
+ <span class="stat-chip"
+ >{{ $t("interface.rx") }} {{ formatBytes(iface._stats?.rxb ?? 0) }}</span
+ >
+ <span v-if="iface.type === 'RNodeInterface' && iface._stats?.noise_floor" class="stat-chip"
+ >{{ $t("interface.noise") }} {{ iface._stats?.noise_floor }} dBm</span
+ >
+ <span v-if="iface._stats?.clients != null" class="stat-chip"
+ >{{ $t("interface.clients") }} {{ iface._stats?.clients }}</span
+ >
+ </div>
+ <div v-if="iface._stats?.ifac_signature" class="ifac-line">
+ <span class="text-emerald-500 font-semibold">{{ iface._stats.ifac_size * 8 }}-bit IFAC</span>
+ <span v-if="iface._stats?.ifac_netname">• {{ iface._stats.ifac_netname }}</span>
+ <span>•</span>
+ <button
+ type="button"
+ class="text-blue-500 hover:underline"
+ @click="onIFACSignatureClick(iface._stats.ifac_signature)"
+ >
+ {{ iface._stats.ifac_signature.slice(0, 8) }}…{{ iface._stats.ifac_signature.slice(-8) }}
+ </button>
+ </div>
</div>
</div>
- <div class="flex flex-col sm:flex-row gap-2 sm:items-center items-end sm:shrink-0 justify-end sm:justify-end relative">
+ <div
+ class="flex flex-col sm:flex-row gap-2 sm:items-center items-end sm:shrink-0 justify-end sm:justify-end relative"
+ >
<button
v-if="isInterfaceEnabled(iface)"
type="button"
diff --git a/tests/backend/test_startup_advanced.py b/tests/backend/test_startup_advanced.py
index 0e692a2a..045ae8a9 100644
--- a/tests/backend/test_startup_advanced.py
+++ b/tests/backend/test_startup_advanced.py
@@ -210,8 +210,8 @@ def test_identity_loading_fallback(mock_rns, temp_dir):
mock_gen_id = MagicMock()
mock_gen_id.hash.hex.return_value = "generated_hash"
mock_gen_id.get_private_key.return_value = b"private_key"
- mock_id_class.side_effect = (
- lambda create_keys=False: mock_gen_id if create_keys else MagicMock()
+ mock_id_class.side_effect = lambda create_keys=False: (
+ mock_gen_id if create_keys else MagicMock()
)
# Mock sys.argv to use default behavior (random generation)
diff --git a/tests/frontend/AppPropagationSync.test.js b/tests/frontend/AppPropagationSync.test.js
index ae3c48a2..f6c65483 100644
--- a/tests/frontend/AppPropagationSync.test.js
+++ b/tests/frontend/AppPropagationSync.test.js
@@ -82,7 +82,7 @@ describe("App propagation sync metrics", () => {
vi.advanceTimersByTime(600);
expect(ToastUtils.success).toHaveBeenCalledWith(
- "Sync complete. 8 messages received. (3 stored, 2 confirmations, 3 hidden)",
+ "Sync complete. 8 messages received. (3 stored, 2 confirmations, 3 hidden)"
);
expect(ToastUtils.error).not.toHaveBeenCalled();
});
diff --git a/tests/frontend/BlockedPage.test.js b/tests/frontend/BlockedPage.test.js
index c39876a0..073bd280 100644
--- a/tests/frontend/BlockedPage.test.js
+++ b/tests/frontend/BlockedPage.test.js
@@ -2,13 +2,15 @@ import { mount, flushPromises } from "@vue/test-utils";
import { describe, it, expect, vi, beforeEach } from "vitest";
import BlockedPage from "../../meshchatx/src/frontend/components/blocked/BlockedPage.vue";
-vi.mock("../../meshchatx/src/frontend/js/DialogUtils", () => ({ default: { confirm: vi.fn().mockResolvedValue(true) } }));
+vi.mock("../../meshchatx/src/frontend/js/DialogUtils", () => ({
+ default: { confirm: vi.fn().mockResolvedValue(true) },
+}));
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({ default: { success: vi.fn(), error: vi.fn() } }));
vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
default: { formatTimeAgo: (d) => "1h ago" },
}));
-const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
+const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
function mountBlockedPage() {
return mount(BlockedPage, {
@@ -23,10 +25,8 @@ describe("BlockedPage UI", () => {
beforeEach(() => {
vi.clearAllMocks();
global.axios.get = vi.fn().mockImplementation((url) => {
- if (url === "/api/v1/blocked-destinations")
- return Promise.resolve({ data: { blocked_destinations: [] } });
- if (url === "/api/v1/reticulum/blackhole")
- return Promise.resolve({ data: { blackholed_identities: {} } });
+ if (url === "/api/v1/blocked-destinations") return Promise.resolve({ data: { blocked_destinations: [] } });
+ if (url === "/api/v1/reticulum/blackhole") return Promise.resolve({ data: { blackholed_identities: {} } });
return Promise.resolve({ data: {} });
});
});
@@ -41,7 +41,7 @@ describe("BlockedPage UI", () => {
it("renders search input and refresh button", async () => {
const wrapper = mountBlockedPage();
await flushPromises();
- expect(wrapper.find("input[type=\"text\"]").exists()).toBe(true);
+ expect(wrapper.find('input[type="text"]').exists()).toBe(true);
expect(wrapper.find("button").exists()).toBe(true);
});
@@ -56,8 +56,7 @@ describe("BlockedPage UI", () => {
global.axios.get = vi.fn().mockImplementation((url, opts) => {
if (url === "/api/v1/blocked-destinations")
return Promise.resolve({ data: { blocked_destinations: ["abc123"] } });
- if (url === "/api/v1/reticulum/blackhole")
- return Promise.resolve({ data: { blackholed_identities: {} } });
+ if (url === "/api/v1/reticulum/blackhole") return Promise.resolve({ data: { blackholed_identities: {} } });
if (url === "/api/v1/announces" && opts?.params?.identity_hash === "abc123")
return Promise.resolve({
data: {
@@ -82,7 +81,7 @@ describe("BlockedPage UI", () => {
it("search input binds to searchQuery", async () => {
const wrapper = mountBlockedPage();
await flushPromises();
- const input = wrapper.find("input[type=\"text\"]");
+ const input = wrapper.find('input[type="text"]');
await input.setValue("test");
expect(wrapper.vm.searchQuery).toBe("test");
});
diff --git a/tests/frontend/ConfirmDialog.test.js b/tests/frontend/ConfirmDialog.test.js
index 3df48ff2..9e580288 100644
--- a/tests/frontend/ConfirmDialog.test.js
+++ b/tests/frontend/ConfirmDialog.test.js
@@ -8,7 +8,7 @@ vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({
import GlobalEmitter from "../../meshchatx/src/frontend/js/GlobalEmitter";
-const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
+const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
function mountDialog() {
return mount(ConfirmDialog, {
diff --git a/tests/frontend/DropDownMenu.test.js b/tests/frontend/DropDownMenu.test.js
index 91ff2ec9..63701eb0 100644
--- a/tests/frontend/DropDownMenu.test.js
+++ b/tests/frontend/DropDownMenu.test.js
@@ -5,8 +5,8 @@ import DropDownMenu from "../../meshchatx/src/frontend/components/DropDownMenu.v
function mountDropDown(slots = {}) {
return mount(DropDownMenu, {
slots: {
- button: "<button type=\"button\">Menu</button>",
- items: "<div class=\"menu-item\">Item 1</div>",
+ button: '<button type="button">Menu</button>',
+ items: '<div class="menu-item">Item 1</div>',
...slots,
},
global: {
@@ -38,7 +38,7 @@ describe("DropDownMenu UI", () => {
});
it("renders items slot when open", async () => {
- const wrapper = mountDropDown({ items: "<div class=\"custom-item\">Custom</div>" });
+ const wrapper = mountDropDown({ items: '<div class="custom-item">Custom</div>' });
await wrapper.find("button").trigger("click");
expect(wrapper.find(".custom-item").exists()).toBe(true);
expect(wrapper.text()).toContain("Custom");
diff --git a/tests/frontend/IconButton.test.js b/tests/frontend/IconButton.test.js
index 88a0de46..b899058c 100644
--- a/tests/frontend/IconButton.test.js
+++ b/tests/frontend/IconButton.test.js
@@ -4,13 +4,13 @@ import IconButton from "../../meshchatx/src/frontend/components/IconButton.vue";
function mountIconButton(slots = {}) {
return mount(IconButton, {
- slots: { default: slots.default ?? "<span class=\"icon\">X</span>" },
+ slots: { default: slots.default ?? '<span class="icon">X</span>' },
});
}
describe("IconButton UI", () => {
it("renders button with slot content", () => {
- const wrapper = mountIconButton({ default: "<span class=\"icon\">+</span>" });
+ const wrapper = mountIconButton({ default: '<span class="icon">+</span>' });
expect(wrapper.find("button").exists()).toBe(true);
expect(wrapper.find(".icon").exists()).toBe(true);
expect(wrapper.text()).toContain("+");
diff --git a/tests/frontend/LoadTimePerformance.test.js b/tests/frontend/LoadTimePerformance.test.js
index c84645b8..a13fc3d5 100644
--- a/tests/frontend/LoadTimePerformance.test.js
+++ b/tests/frontend/LoadTimePerformance.test.js
@@ -34,8 +34,8 @@ vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({
default: { on: vi.fn(), off: vi.fn(), emit: vi.fn() },
}));
-const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
-const LxmfUserIcon = { template: "<div class=\"lxmf-icon\"></div>" };
+const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
+const LxmfUserIcon = { template: '<div class="lxmf-icon"></div>' };
function makePropagationNode(i) {
return {
@@ -107,7 +107,9 @@ describe("Load time with prefilled data", () => {
expect(wrapper.vm.paginatedNodes.length).toBe(20);
expect(loadMs).toBeLessThan(MAX_PROP_NODES_MS);
if (process.env.CI !== "true") {
- console.log(`Propagation nodes: ${count} nodes loaded in ${loadMs.toFixed(0)}ms (max ${MAX_PROP_NODES_MS}ms)`);
+ console.log(
+ `Propagation nodes: ${count} nodes loaded in ${loadMs.toFixed(0)}ms (max ${MAX_PROP_NODES_MS}ms)`
+ );
}
});
});
@@ -148,7 +150,9 @@ describe("Load time with prefilled data", () => {
expect(wrapper.vm.displayedConversations.length).toBe(count);
expect(end - start).toBeLessThan(MAX_MESSAGES_ANNOUNCES_MS);
if (process.env.CI !== "true") {
- console.log(`Messages: ${count} conversations in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`);
+ console.log(
+ `Messages: ${count} conversations in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`
+ );
}
});
});
@@ -190,7 +194,9 @@ describe("Load time with prefilled data", () => {
expect(wrapper.vm.searchedPeers.length).toBe(count);
expect(end - start).toBeLessThan(MAX_MESSAGES_ANNOUNCES_MS);
if (process.env.CI !== "true") {
- console.log(`Announces (messages): ${count} peers in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`);
+ console.log(
+ `Announces (messages): ${count} peers in ${(end - start).toFixed(0)}ms (max ${MAX_MESSAGES_ANNOUNCES_MS}ms)`
+ );
}
});
});
@@ -220,7 +226,7 @@ describe("Load time with prefilled data", () => {
components: {
MaterialDesignIcon,
IconButton: { template: "<button></button>" },
- DropDownMenu: { template: "<div><slot name=\"button\"/><slot name=\"items\"/></div>" },
+ DropDownMenu: { template: '<div><slot name="button"/><slot name="items"/></div>' },
DropDownMenuItem: { template: "<div></div>" },
},
mocks: { $t: (key) => key },
@@ -233,7 +239,9 @@ describe("Load time with prefilled data", () => {
expect(wrapper.vm.searchedNodes.length).toBe(count);
expect(end - start).toBeLessThan(MAX_NOMADNET_NODES_MS);
if (process.env.CI !== "true") {
- console.log(`NomadNet nodes: ${count} nodes in ${(end - start).toFixed(0)}ms (max ${MAX_NOMADNET_NODES_MS}ms)`);
+ console.log(
+ `NomadNet nodes: ${count} nodes in ${(end - start).toFixed(0)}ms (max ${MAX_NOMADNET_NODES_MS}ms)`
+ );
}
});
});
diff --git a/tests/frontend/MessagesSidebar.test.js b/tests/frontend/MessagesSidebar.test.js
index 0f6b3876..cc42f203 100644
--- a/tests/frontend/MessagesSidebar.test.js
+++ b/tests/frontend/MessagesSidebar.test.js
@@ -20,8 +20,8 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
},
}));
-const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
-const LxmfUserIcon = { template: "<div class=\"lxmf-icon\"></div>" };
+const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
+const LxmfUserIcon = { template: '<div class="lxmf-icon"></div>' };
function defaultProps(overrides = {}) {
return {
@@ -89,7 +89,9 @@ describe("MessagesSidebar UI", () => {
await announcesTab.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.vm.tab).toBe("announces");
- expect(wrapper.text()).toMatch(/messages\.search_placeholder_announces|messages\.no_peers_discovered|messages\.waiting_for_announce/);
+ expect(wrapper.text()).toMatch(
+ /messages\.search_placeholder_announces|messages\.no_peers_discovered|messages\.waiting_for_announce/
+ );
});
it("emits folder-click when All Messages is clicked", async () => {
@@ -154,7 +156,7 @@ describe("MessagesSidebar UI", () => {
],
});
await wrapper.vm.$nextTick();
- const selectionBtn = wrapper.find("button[title=\"Selection Mode\"]");
+ const selectionBtn = wrapper.find('button[title="Selection Mode"]');
expect(selectionBtn.exists()).toBe(true);
await selectionBtn.trigger("click");
await wrapper.vm.$nextTick();
@@ -182,6 +184,9 @@ describe("MessagesSidebar UI", () => {
const row = wrapper.find(".conversation-item");
await row.trigger("click");
expect(wrapper.emitted("conversation-click")).toBeTruthy();
- expect(wrapper.emitted("conversation-click")[0][0]).toMatchObject({ destination_hash: "dest1", display_name: "Bob" });
+ expect(wrapper.emitted("conversation-click")[0][0]).toMatchObject({
+ destination_hash: "dest1",
+ display_name: "Bob",
+ });
});
});
diff --git a/tests/frontend/NotificationBell.test.js b/tests/frontend/NotificationBell.test.js
index dbeb02f3..b789cd3b 100644
--- a/tests/frontend/NotificationBell.test.js
+++ b/tests/frontend/NotificationBell.test.js
@@ -10,7 +10,7 @@ vi.mock("../../meshchatx/src/frontend/js/Utils", () => ({
default: { formatTimeAgo: (d) => "1h ago" },
}));
-const MaterialDesignIcon = { template: "<div class=\"mdi\"></div>", props: ["iconName"] };
+const MaterialDesignIcon = { template: '<div class="mdi"></div>', props: ["iconName"] };
function mountBell(options = {}) {
return mount(NotificationBell, {
diff --git a/tests/frontend/SidebarLink.test.js b/tests/frontend/SidebarLink.test.js
index 00b21ad5..8402e620 100644
--- a/tests/frontend/SidebarLink.test.js
+++ b/tests/frontend/SidebarLink.test.js
@@ -5,8 +5,7 @@ import SidebarLink from "../../meshchatx/src/frontend/components/SidebarLink.vue
const RouterLinkStub = {
name: "RouterLinkStub",
props: ["to"],
- template:
- '<a href="#" @click.prevent><slot :href="\'#\'" :navigate="navigate" :isActive="false"/></a>',
+ template: '<a href="#" @click.prevent><slot :href="\'#\'" :navigate="navigate" :isActive="false"/></a>',
methods: {
navigate(e) {
if (e) e.preventDefault();
@@ -18,7 +17,7 @@ function mountSidebarLink(props = {}, slots = {}) {
return mount(SidebarLink, {
props: { to: { name: "messages" }, ...props },
slots: {
- icon: "<span class=\"icon-slot\">I</span>",
+ icon: '<span class="icon-slot">I</span>',
text: "Messages",
...slots,
},
diff --git a/tests/frontend/VisualizerOptimization.test.js b/tests/frontend/VisualizerOptimization.test.js
index a85a65a9..8d3cee0e 100644
--- a/tests/frontend/VisualizerOptimization.test.js
+++ b/tests/frontend/VisualizerOptimization.test.js
@@ -243,51 +243,51 @@ describe("NetworkVisualiser Optimization and Abort", () => {
expect(end - start).toBeLessThan(100); // Should be very fast
});
- it("performance: icon cache hit vs miss for 500 nodes", async () => {
- vi.spyOn(NetworkVisualiser.methods, "init").mockImplementation(() => {});
- const wrapper = mountVisualiser();
-
- // Setup 500 nodes with the same icon
- const iconInfo = { icon_name: "test", foreground_colour: "#000", background_colour: "#fff" };
- wrapper.vm.pathTable = Array.from({ length: 500 }, (_, i) => ({ hash: `h${i}`, interface: "eth0", hops: 1 }));
- wrapper.vm.announces = wrapper.vm.pathTable.reduce((acc, cur) => {
- acc[cur.hash] = {
- destination_hash: cur.hash,
- aspect: "lxmf.delivery",
- display_name: "node",
- lxmf_user_icon: iconInfo,
- };
- return acc;
- }, {});
- wrapper.vm.conversations = wrapper.vm.pathTable.reduce((acc, cur) => {
- acc[cur.hash] = { lxmf_user_icon: iconInfo };
- return acc;
- }, {});
-
- // Mock createIconImage to have some delay for the "miss" case
- wrapper.vm.createIconImage = vi.fn().mockImplementation(async () => {
- // Add a tiny delay to ensure "miss" is always measurable
- await new Promise((r) => setTimeout(r, 0));
- return "blob:mock-icon";
- });
-
- const startMiss = performance.now();
- await wrapper.vm.processVisualization();
- const endMiss = performance.now();
- const missTime = endMiss - startMiss;
-
- // Second run will hit the cache check in processVisualization
- // so it won't even call createIconImage.
- const startHit = performance.now();
- await wrapper.vm.processVisualization();
- const endHit = performance.now();
- const hitTime = endHit - startHit;
-
- console.log(`Icon cache MISS for 500 nodes: ${missTime.toFixed(2)}ms`);
- console.log(`Icon cache HIT for 500 nodes: ${hitTime.toFixed(2)}ms`);
-
- // Cache hit should be significantly faster, but we allow for some
- // environmental noise in CI environments.
- expect(hitTime).toBeLessThan(missTime + 200);
+ it("performance: icon cache hit vs miss for 500 nodes", async () => {
+ vi.spyOn(NetworkVisualiser.methods, "init").mockImplementation(() => {});
+ const wrapper = mountVisualiser();
+
+ // Setup 500 nodes with the same icon
+ const iconInfo = { icon_name: "test", foreground_colour: "#000", background_colour: "#fff" };
+ wrapper.vm.pathTable = Array.from({ length: 500 }, (_, i) => ({ hash: `h${i}`, interface: "eth0", hops: 1 }));
+ wrapper.vm.announces = wrapper.vm.pathTable.reduce((acc, cur) => {
+ acc[cur.hash] = {
+ destination_hash: cur.hash,
+ aspect: "lxmf.delivery",
+ display_name: "node",
+ lxmf_user_icon: iconInfo,
+ };
+ return acc;
+ }, {});
+ wrapper.vm.conversations = wrapper.vm.pathTable.reduce((acc, cur) => {
+ acc[cur.hash] = { lxmf_user_icon: iconInfo };
+ return acc;
+ }, {});
+
+ // Mock createIconImage to have some delay for the "miss" case
+ wrapper.vm.createIconImage = vi.fn().mockImplementation(async () => {
+ // Add a tiny delay to ensure "miss" is always measurable
+ await new Promise((r) => setTimeout(r, 0));
+ return "blob:mock-icon";
});
+
+ const startMiss = performance.now();
+ await wrapper.vm.processVisualization();
+ const endMiss = performance.now();
+ const missTime = endMiss - startMiss;
+
+ // Second run will hit the cache check in processVisualization
+ // so it won't even call createIconImage.
+ const startHit = performance.now();
+ await wrapper.vm.processVisualization();
+ const endHit = performance.now();
+ const hitTime = endHit - startHit;
+
+ console.log(`Icon cache MISS for 500 nodes: ${missTime.toFixed(2)}ms`);
+ console.log(`Icon cache HIT for 500 nodes: ${hitTime.toFixed(2)}ms`);
+
+ // Cache hit should be significantly faster, but we allow for some
+ // environmental noise in CI environments.
+ expect(hitTime).toBeLessThan(missTime + 200);
+ });
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────